package com.citahub.link;

import com.citahub.cita.abi.FunctionEncoder;
import com.citahub.cita.abi.datatypes.Function;
import com.citahub.cita.protocol.core.methods.response.TransactionReceipt;
import com.citahub.cita.utils.HexUtil;
import com.citahub.constant.TransactionConstant;
import com.citahub.constant.TransactionConstant.Type;
import com.citahub.contract.ContractCall;
import com.citahub.util.ChainUtils;
import com.citahub.util.InternalBlock;
import com.citahub.util.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import link.luyu.protocol.algorithm.ecdsa.secp256k1.SignatureData;
import link.luyu.protocol.algorithm.sm2.SM2WithSM3;
import link.luyu.protocol.common.STATUS;
import link.luyu.protocol.link.Connection;
import link.luyu.protocol.link.Driver;
import link.luyu.protocol.network.Account;
import link.luyu.protocol.network.CallRequest;
import link.luyu.protocol.network.CallResponse;
import link.luyu.protocol.network.Events;
import link.luyu.protocol.network.Receipt;
import link.luyu.protocol.network.Resource;
import link.luyu.protocol.network.Transaction;
import com.citahub.cita.crypto.sm2.SM2;
import com.citahub.cita.crypto.sm2.SM2Keys;
import com.citahub.cita.utils.Numeric;
import com.citahub.config.ResourceStubConfig;
import com.citahub.config.ResourcesStubConfigParser;
import link.luyu.toolkit.abi.ContractABI;
import link.luyu.toolkit.abi.FunctionABI;

public class CITADriver implements Driver {
    private Connection connection;

    private static final ObjectMapper objectMapper = new ObjectMapper();

    public CITADriver(Connection connection, Map<String, Object> properties) throws Exception {
        this.connection = connection;
    }

    public void start() throws RuntimeException {

    }

    public void stop() throws RuntimeException {

    }

    private static byte[] join(byte[]... params) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] res = null;
        try {
            for (int i = 0; i < params.length; i++) {
                baos.write(params[i]);
            }
            res = baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return res;
    }

    @Override
    public void sendTransaction(Account account, Transaction transaction, ReceiptCallback callback) {
        String contractAddress = checkContractWhiteList(transaction.getPath());
        if (StringUtils.isEmpty(contractAddress)) {
          callback.onResponse(STATUS.INTERNAL_ERROR,
                            "no contract found",
                            null);
        } else {
            String[] transactionData = new String[1];
            try {
                if (StringUtils.isEmpty(transaction.getMethod()) && transaction.getArgs().length > 0) {
                    transactionData[0] = transaction.getArgs()[0];
                } else {
                    CompletableFuture<String> abiFuture = new CompletableFuture<>();
                    connection.asyncSend(contractAddress, Type.GET_ABI, null, (errorCode, msg, responseData) -> {
                        if (errorCode != STATUS.OK) {
                            callback.onResponse(errorCode, msg, null);
                            abiFuture.complete(null);
                        } else {
                            String raw_abi = new String(responseData, StandardCharsets.UTF_8);
                            String abi = Utils.hexStr2Str(Utils.hexRemove0x(raw_abi));
                            abiFuture.complete(abi);
                        }
                    });
                    String abi = abiFuture.get(10, TimeUnit.SECONDS);
                    transactionData[0] = FunctionEncoder.encode(
                            Utils.convertFunction(
                                    abi, transaction.getMethod(), transaction.getArgs()));
                }
            } catch (Exception e) {
                throw new RuntimeException("processing transactionData failed, error: " + e.getMessage());
            }
            connection.asyncSend("", Type.GET_BLOCK_NUMBER, null, (errorCode1, msg1, responseData1) -> {
                if(errorCode1 != STATUS.OK) {
                  callback.onResponse(errorCode1, msg1, null);
                } else {
                  long blockNumber = Utils.bytesToLong(responseData1);
                  com.citahub.cita.protocol.core.methods.request.Transaction tx =
                      new com.citahub.cita.protocol.core.methods.request.Transaction(
                          contractAddress,
                          ChainUtils.getNonce(),
                          10000000L,
                          blockNumber + 88,
                          ChainUtils.getVersion(),
                          ChainUtils.getChainId(),
                          "0",
                          transactionData[0]);

                  try {
                    byte[] bsTx = tx.serializeRawTransaction(false);
                    CompletableFuture<byte[]> future = new CompletableFuture();

                    byte[] pubKey = account.getPubKey();
                    byte[] prepareMessage = SM2WithSM3.prepareMessage(pubKey, bsTx);

                    account.sign(
                        prepareMessage,
                        new Account.SignCallback() {
                          @Override
                          public void onResponse(int status, String message, byte[] signBytes) {
                            if (status != STATUS.OK) {
                              future.complete(null);
                            } else {
                              future.complete(signBytes);
                            }
                          }
                        });

                    byte[] luyuSignBytes = future.get(30, TimeUnit.SECONDS);

                    SignatureData luyuSignData = SignatureData.parseFrom(luyuSignBytes);
                    SM2.Signature signature = new SM2.Signature(luyuSignData.getR(), luyuSignData.getS());
                    byte[] sig = join(HexUtil.hexToBytes(signature.getSign()), pubKey);
                    String raw_tx = tx.serializeUnverifiedTransaction(sig, bsTx);

                    connection.asyncSend(
                        transaction.getPath(),
                        Type.SEND_TRANSACTION,
                        raw_tx.getBytes(StandardCharsets.UTF_8),
                        (errorCode2, msg2, responseData2) -> {
                          // todo verify transaction on-chain proof
                          if(errorCode2 != STATUS.OK) {
                            callback.onResponse(errorCode2, msg2, null);
                          } else {
                            Receipt receipt = new Receipt();
                            receipt.setBlockNumber(123);
                            receipt.setMethod(transaction.getMethod());
                            receipt.setArgs(transaction.getArgs());
                            receipt.setPath(transaction.getPath());
                            receipt.setCode(0); // SUCCESS
                            receipt.setMessage("Success");
                            receipt.setTransactionBytes(responseData2);
                            receipt.setTransactionHash(new String(responseData2));
                            callback.onResponse(STATUS.OK, "Success", receipt);
                          }
                        });
                  } catch (Exception e) {
                    e.printStackTrace();
                    throw new RuntimeException("sign and send failed,error" + e.getMessage());
                  }
                }
            });
        }
    }

    @Override
    public void call(Account account, CallRequest callRequest, CallResponseCallback callback) {
        String contractAddress = checkContractWhiteList(callRequest.getPath());
        if (StringUtils.isEmpty(contractAddress)) {
          callback.onResponse(STATUS.INTERNAL_ERROR,
                            "no contract found",
                            null);
        } else {
          connection.asyncSend(contractAddress, Type.GET_ABI, null, (errorCode, msg, responseData) -> {
            if(errorCode != STATUS.OK) {
              callback.onResponse(errorCode, msg, null);
            } else {
              String raw_abi = new String(responseData, StandardCharsets.UTF_8);
              String abi = Utils.hexStr2Str(Utils.hexRemove0x(raw_abi));
              ContractABI ctAbi = new ContractABI(abi);
              FunctionABI funAbi;
              try {
                funAbi = ctAbi.getFunctions(callRequest.getMethod()).get(0);
              } catch (IndexOutOfBoundsException ioe) {
                callback.onResponse(
                    STATUS.INTERNAL_ERROR,
                    "no method found",
                    null);
                return;
              }
              Function function = Utils.convertFunction(abi, callRequest.getMethod(), callRequest.getArgs());
              ContractCall call = new ContractCall(contractAddress, FunctionEncoder.encode(function));
              byte[] pubKey = account.getPubKey();
              String sender = SM2Keys.getAddress(Numeric.toHexStringWithPrefixZeroPadded(new BigInteger(1, pubKey), 128));
              call.setSender("0x" + sender);
              try {
                byte[] data = objectMapper.writeValueAsBytes(call);
                connection.asyncSend(
                    callRequest.getPath(),
                    Type.CALL_TRANSACTION,
                    data,
                    (errorCode1, message1, responseData1) -> {
                      CallResponse callResponse = new CallResponse();
                      if (responseData1 != null) {
                        String resp = new String(responseData1);
                        if (!resp.equals("0x")) {
                          callResponse.setResult(funAbi.decodeOutput(resp));
                        }
                      }
                      callResponse.setCode(0); // original receipt status
                      callResponse.setMessage("Success");
                      callResponse.setMethod(callRequest.getMethod());
                      callResponse.setArgs(callRequest.getArgs());
                      callResponse.setPath(callRequest.getPath());
                      callback.onResponse(STATUS.OK, "Success", callResponse);
                    });
              } catch (JsonProcessingException e) {
                callback.onResponse(
                    STATUS.INTERNAL_ERROR, "serialize failed,error:" + e.getMessage(), null);
                throw new RuntimeException("serialize failed");
              }
            }
          });
        }

    }

    private String checkContractWhiteList(String path) {
        CompletableFuture<List<Resource>> listResourceFuture = new CompletableFuture<>();
        List<Resource> resourceList = Lists.newArrayList();
        try {
            connection.asyncSend("", Type.GET_LIST_RESOURCES, null, (errorCode, message, responseData) -> {
                if(errorCode != STATUS.OK) {
                    listResourceFuture.complete(null);
                } else {
                    objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                    try {
                        List<Resource> resources = objectMapper.readValue(responseData, new TypeReference<List<Resource>>(){});
                        listResourceFuture.complete(resources);
                    } catch (IOException e) {
                        throw new RuntimeException("get list resources, error: " + e.getMessage());
                    }
                }
            });
            resourceList = listResourceFuture.get(10, TimeUnit.SECONDS);
        } catch (Exception e) {
            throw new RuntimeException("get list resources, error: " + e.getMessage());
        }
        if (CollectionUtils.isEmpty(resourceList)) {
            return StringUtils.EMPTY;
        }
        Resource resource = resourceList.stream().filter(r -> r.getPath().equals(path)).findFirst().orElse(null);
        if (Objects.isNull(resource)){
            return StringUtils.EMPTY;
        }
        return (String) resource.getProperties().get("contractAddress");
    }

    @Override
    public void getTransactionReceipt(String txHash, ReceiptCallback callback) {
        connection.asyncSend(
                null,
                TransactionConstant.Type.GET_TRANSACTION_RECEIPT,
                txHash.getBytes(),
                (errorCode, message, responseData) -> {
                    try {
                        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                        TransactionReceipt transactionReceipt =
                                objectMapper.readValue(responseData, TransactionReceipt.class);
                        Receipt receipt = new Receipt();
                        receipt.setResult(new String[] {new String(responseData)});
                        receipt.setBlockNumber(
                                Long.parseLong(transactionReceipt.getBlockNumberRaw()));
                        receipt.setCode(0); // SUCCESS
                        receipt.setMessage("Success");
                        receipt.setTransactionHash(transactionReceipt.getTransactionHash());
                        connection.asyncSend(null, TransactionConstant.Type.GET_TRANSACTION, txHash.getBytes(),
                            (tranErrorCode, tranMessage, tranResponseData) -> {
                              objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                              try {
                                com.citahub.cita.protocol.core.methods.response.Transaction transaction =
                                    objectMapper.readValue(tranResponseData, com.citahub.cita.protocol.core.methods.response.Transaction.class);
                                receipt.setTransactionBytes(Numeric.hexStringToByteArray(transaction.getContent()));
                                callback.onResponse(STATUS.OK, "Success", receipt);
                              } catch (IOException e) {
                                callback.onResponse(
                                    STATUS.INTERNAL_ERROR,
                                    "deserialize failed,error:" + e.getMessage(),
                                    null);
                              }
                            });
                    } catch (IOException e) {
                        callback.onResponse(
                                STATUS.INTERNAL_ERROR,
                                "deserialize failed,error:" + e.getMessage(),
                                null);
                    }
                });
    }

    @Override
    public void getBlockByHash(String blockHash, BlockCallback callback) {
        connection.asyncSend(blockHash, Type.GET_BLOCK_BY_HASH, null, (errorCode, message, responseData) -> {
            if(errorCode != STATUS.OK) {
                callback.onResponse(errorCode, message, null);
            } else {
                InternalBlock block = (InternalBlock)Utils.toObject(responseData);
                callback.onResponse(STATUS.OK, "Success", block.toBlock());
            }
        });
    }

    @Override
    public void getBlockByNumber(long blockNumber, BlockCallback callback) {
        connection.asyncSend("", Type.GET_BLOCK_BY_NUMBER, Utils.longToBytes(blockNumber), (errorCode, message, responseData) -> {
            if(errorCode != STATUS.OK) {
                callback.onResponse(errorCode, message, null);
            } else {
                InternalBlock block = (InternalBlock)Utils.toObject(responseData);
                callback.onResponse(STATUS.OK, "Success", block.toBlock());
            }
        });
    }

    @Override
    public long getBlockNumber() {
        CompletableFuture<byte[]> getBlockNumberFuture = new CompletableFuture<>();
        connection.asyncSend("", Type.GET_BLOCK_NUMBER, null, new Connection.Callback() {
            @Override
            public void onResponse(int errorCode, String message, byte[] responseData) {
                if (errorCode != STATUS.OK) {
                    getBlockNumberFuture.complete(null);
                } else {
                    getBlockNumberFuture.complete(responseData);
                }
            }
        });

        byte[] bsBlockNumber;
        try {
            bsBlockNumber = getBlockNumberFuture.get(20, TimeUnit.SECONDS);
            if (bsBlockNumber == null) {
                return -1;
            }
        } catch (Exception e) {
            return -1;
        }

        long blockNumber = Utils.bytesToLong(bsBlockNumber);
        return blockNumber;
    }

    @Override
    public String getType() {
        return "CITA";
    }

    @Override
    public String getSignatureType() {
        return "SM2_WITH_SM3";
    }

    @Override
    public void listResources(ResourcesCallback callback) {
        connection.asyncSend("", Type.GET_LIST_RESOURCES, null, (errorCode, message, responseData) -> {
            if(errorCode != STATUS.OK) {
                callback.onResponse(errorCode, message, null);
            } else {
                objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                try {
                    List<Resource> resources = objectMapper.readValue(responseData, new TypeReference<List<Resource>>(){});
                    callback.onResponse(STATUS.OK, "Success", resources.toArray(new Resource[0]));
                } catch (IOException e) {
                    throw new RuntimeException("get list resources, error: " + e.getMessage());
                }
            }
        });
    }

    @Override
    public void registerEvents(Events events) {}
}
